package spitfire.ksim.network;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>
 * KNetwork is a virtual network that manages the data transfer process among
 * hosts connected into this network.
 * All terminal hosts in the network should implement {@link KNetworkModule} 
 * interface. When a host register into the network, it will get a unique 
 * {@link KNetAddress} in this network.
 * </p>
 * 
 * @author Adam
 *
 */
public class KNetwork {

	private static final int INFINITY = Integer.MAX_VALUE;
	
	private Map<KNetAddress, KNetworkModule> addrModuleMap = new ConcurrentHashMap<KNetAddress, KNetworkModule>();
	
	/**
	 * Register the given host on the network.
	 * 
	 * @param module
	 *            - the host network module to register.
	 * @return the allocated network address for this host.
	 */
	public KNetAddress register(KNetworkModule module) {
		KNetAddress addr = KNetAddress.getNextNetAddr();
		synchronized (addrModuleMap) {
			addrModuleMap.put(addr, module);
		}
		return addr;
	}

	/**
	 * Deregister the given host.
	 * 
	 * @param module - the host network module to deregister.
	 */
	public void deregister(KNetworkModule module) {
		synchronized (addrModuleMap) {
			addrModuleMap.remove(module.getNetAddr());
		}
	}
	
	/**
	 * Deregister the host allocated with the given network address.
	 * 
	 * @param netAddr - network address of the host to deregister.
	 */
	public void deregister(KNetAddress netAddr) {
		synchronized (addrModuleMap) {
			addrModuleMap.remove(netAddr);
		}
	}
	
	/**
	 * Return all the host network modules in the network.
	 * 
	 * @return the collection of all network modules.
	 */
	public Collection<KNetworkModule> getNetModules() {
		synchronized (addrModuleMap) {
			return addrModuleMap.values();
		}
	}
	
	/**
	 * Transmit the given {@link KPacket}.
	 * 
	 * @param packet - the packet to transmit.
	 */
	public void transmit(KPacket packet) {
		/* TODO The accessibility from source to destination is calculated by
		 * Warshall algorithm. 
		 * Optimization is available with other shortest path algorithm (djikastra)
 		 */
		synchronized(addrModuleMap) {
			int[][] adjacencyClosure = calAdjacencyClosure();

			KNetAddress src = packet.getSrcAddr();
			KNetAddress dst = packet.getDstAddr();
			if (packet.getDstAddr().equals(KNetAddress.BROADCAST_ADDR)) {
				// Broadcast packet
				for (KNetworkModule host : addrModuleMap.values()) {
					if (adjacencyClosure[src.toInt()][host.getNetAddr().toInt()] != INFINITY) {
						host.recv(packet);
					}
				}
			} else if (!addrModuleMap.containsKey(packet.getDstAddr())) {
				// destination host doesn't exist
				// Do nothing
			} else {
				// destination host exists
				if (adjacencyClosure[src.toInt()][dst.toInt()] != INFINITY) {
					KNetworkModule dstHost = addrModuleMap.get(dst);
					dstHost.recv(packet);
				}
			}
		}
	}

	/**
	 * Calculate closure for the adjacency matrix of hosts in the network.
	 * @return the closure of adjacency matrix.
	 */
	private int[][] calAdjacencyClosure() {
		int size = KNetAddress.getAddrToIntMax();
		// generate new matrix
		int[][] mat = new int[size][];
		for (int i = 0; i < size; i++) {
			mat[i] = new int[size];
		}
		// calculate adjacency
		for (KNetworkModule src : addrModuleMap.values()) {
			for (KNetworkModule dst : addrModuleMap.values()) {
				int srcIdx = src.getNetAddr().toInt();
				int dstIdx = dst.getNetAddr().toInt();
				if (srcIdx == dstIdx) {
					// 0 denotes itself
					mat[srcIdx][dstIdx] = 0;
				} else {
					// 1 denotes adjacent and -1 denotes not adjacent
					mat[srcIdx][dstIdx] = src.adjacentWith(dst) ? 1 : INFINITY;
				}
			}
		}	
		// calculate closure
		for (int k = 0; k < size; k++) {
			for (int i = 0; i < size; i++) {
				for (int j = 0; j < size; j++) {
					int dik = mat[i][k];
					int dkj = mat[k][j];
					if (dik + dkj > 0 && dik + dkj <= mat[i][j]) {
						mat[i][j] = dik + dkj;
					}
				}
			}
		}
		return mat;
	}

//	private void updateClosure() {
//		// copy adjacency matrix to closure
//		int size = adjacency.length;
//		adjacencyClosure = new int[size][];
//		for (int i = 0; i < size; i++) {
//			adjacencyClosure[i] = adjacency[i].clone();
//		}
//		// calculate closure
//		for (int k = 0; k < size; k++) {
//			for (int i = 0; i < size; i++) {
//				for (int j = 0; j < size; j++) {
//					int dik = adjacencyClosure[i][k];
//					int dkj = adjacencyClosure[k][j];
//					if (dik + dkj > 0 && dik + dkj <= adjacencyClosure[i][j]) {
//						adjacencyClosure[i][j] = dik + dkj;
//					}
//				}
//			}
//		}
//	}

	/**
	 * Remove the references to all hosts in the network, and reset {@link KPacket}
	 * ID and {@link KNetAddress} ID.
	 */
	public void destroy() {
		synchronized(addrModuleMap) {
			addrModuleMap.clear();
		}
		KPacket.resetID();
		KNetAddress.resetID();
	}
	
}
